Repository navigation
feat: the scope fence, pasta + nftables per step (step 5, PR 6a) - #22
Conversation
Phase 10, step 5 plan, PR 6 (scope fence). An optional layer registers a function (add_network_fence) that says, for a network step, which addresses it may reach; the step then runs in a network namespace of its own whose only way out is pasta, with nftables letting through only those addresses (excluded ranges dropped first). The mechanism is in the core, the policy in the layer. From the live prototype: pasta's defaults reach the machine's own loopback and map the gateway address to it, so it runs with --no-map-gw -t none -u none -T none -U none. nft loads the rules in pasta's namespace, and bwrap then starts the step with --cap-drop ALL in a user namespace of its own: it cannot change the rules and has no raw sockets (their packets would pass the rules by). setpriv was not used: after it bwrap cannot write its uid map. There is no DNS in the fence -- lookups through the host's resolver would carry data to whoever serves a name -- so the step's names are resolved before it and written into an /etc/hosts of its own, and resolv.conf is empty. A step that must be fenced is refused when the fence cannot be built (no bwrap, pasta or nft; privacy trusted), or when the fence function refuses it or fails. pasta, nft and sh are pinned by codebud integrity pin when the security layer is installed, and checked before each fenced step like the isolation tool. * src/codebud/isolation.py (FENCE_PROGRAMS, Fence, nft_ruleset) (fence_available, add_network_fence, remove_network_fence, fenced) (prepare_with_programs): New. (wrap): Take files to bind and no_capabilities. (prepare): Use prepare_with_programs. * src/codebud/sandbox.py (Sandbox.run_command): Pin the programs prepare_with_programs added. * src/codebud/cli/integrity.py (_programs): Pin the fence's programs. * tests/test_fence.py: New file (live where the tools work). * tests/test_integrity_steps.py: Stub prepare_with_programs. * docs/chapters/configuration.texi (Isolated Steps): The network fence. * docs/chapters/architecture.texi, docs/chapters/development.texi: Mention it.
Phase 10, step 5 plan, PR 6. The scope guard reads a step's targets from its command line; the fence now makes the network itself hold to them, so a tool that reaches further than its command line says -- a redirect, a script, a bug, a plan that slipped past a parser -- meets a closed door. The guard registers it with the core's fence when the engagement opens. Decided with the operator (2026-11-12): a fenced step may reach exactly its targets -- addresses and networks as written, names resolved just before the step (the client's DNS decides; a shared CDN address is reachable while it runs) into the step's own /etc/hosts -- and never an excluded network. Scanning is fenced by default; fence_scanning = false in the engagement gives raw scans back, judged by the guard alone. Lookups (dig, host, whois) need DNS or a registry and are not fenced. Behind the fence there are no raw sockets: nmap needs --unprivileged, and SYN, UDP and OS scans and masscan are refused with that reason -- when the plan is judged, not only when the step runs. The profile's prompt says so. The engagement module's docstring still described the old warn-only scope; it now says what D14i changed. * src/codebud_sec/fence.py: New file. * src/codebud_sec/engagement.py (_BOOL_KEYS, Engagement.fence_scanning): New. (load): Read it. * src/codebud_sec/scope.py (ScopeGuard): Own a ScopeFence; register it on activate, remove it on deactivate; report its problems. * src/codebud_sec/data/profiles/security-pentest.toml (prompt): nmap --unprivileged -n; no masscan behind the fence. * src/Makefile.am: Install fence.py. * tests/test_scope_fence.py: New file. * tests/test_scope.py: The guard alone (fence_scanning = false). * tests/test_scope_web.py, tests/test_security_pentest.py: Reset the registered fences; nmap --unprivileged; masscan and SYN scans refused. * docs/chapters/configuration.texi (Profiles): The fence for scanning. * docs/chapters/architecture.texi, docs/chapters/development.texi: Mention them.
Review summary: correct, no defects foundOne full sweep over Conditions. A network step is fenced ⇔ a registered fence returns a The invariant behind it: a fenced step's packets reach only addresses in Verified, live on this machine:
Erring safe: combined nmap flags like Residual risks (decided):
1778 tests pass, |
|
Merging. The review found no defects. CI is green on all five jobs at |
Phase 10, step 5 plan, PR 6a: the scope fence. The scope guard reads a step's targets from its command line; the fence makes the network itself hold to them. A tool that reaches further than its command line says (a redirect, a script, a bug, a plan that slipped past a parser) meets a closed door. The exploitation tier (T2) comes separately as 6b, behind this fence.
What the live prototype found (all on this machine with passt 2026_10_02)
127.0.0.1-only listener, and to the host through the gateway address. The fence runs pasta with--no-map-gw -t none -u none -T none -U none. With those options alone (no rules at all), the host's loopback, its gateway address and its LAN address are all unreachable: a second, independent layer.setprivcan't be used: after it, bwrap can't write its uid map. Instead, bwrap runs the step with--cap-drop ALLin a child user namespace: no effective or bounding capabilities, and no-new-privileges set.nft flush rulesetis denied. There are no raw sockets either; keeping them would be unsafe, because packet sockets bypass the rules./etc/hosts, andresolv.confis empty. Verified: the step's own name resolves and connects, other names fail, anddig @1.1.1.1is blocked.Commits
29cc2f7Core mechanism (codebud.isolation):Fence,nft_ruleset, and the pasta → nft → bwrap command line;add_network_fence(): a refusal or a crashing hook stops the step;trusted;codebud integrity pinand checked before each fenced step.ac10eb0Policy (codebud_sec/fence.py), as you decided: names are resolved per step; scanning is fenced by default, andfence_scanning = falsegives raw scans back with the guard as their only check.--unprivileged. SYN, UDP and OS scans and masscan are refused with that reason when the plan is judged, not only when the step runs.Verification
make check: 1778 passed.make distcheckpasses;make infobuilds without warnings; ruff is clean.tests/test_fence.pyruns where bwrap, pasta and nft work; it was run here.nmap --unprivilegedand curl run fenced; nmap without--unprivilegedis refused with the reason.